﻿using System;
using System.IO;
using System.Collections.Generic;

namespace yacomp
{
    using SanlieHash.Fast;
    using yacomp.algo;
    using yacomp.helpers;
    using yacomp.os;
    using yacomp.structs;

    public class YacompZip : IDisposable
    {
        internal const uint item_tag = 0x04034b50;
        internal const uint central_tag = 0x02014b50;
        internal const uint end_tag = 0x06054b50;
        internal const uint sign_tag = 0x05054b50;
        internal const uint data_desc_tag = 0x08074b50;

        internal const ushort ver_decompress_none = 0x0000;
        internal const ushort ver_decompress_defalte = 0x0014;
        internal const ushort ver_compress_unix = 0x0314;
        internal const ushort ver_compress_fat = 0x003f;
        internal const ushort flag_none = 0x0000;
        internal const ushort flag_ascii = 0x0008;
        internal const ushort flag_utf8 = 0x0808;
        internal const ushort compress_type_not_compress = 0x0000;
        internal const ushort compress_type_compress_deflate = 0x0008;
        internal const uint unix_attr_path_rwx = 0x41ff0000;
        internal const uint unix_attr_file_rwx = 0x81ff0000;
        internal const uint unix_attr_file_rw  = 0x81b60000;
        internal const uint unix_attr_path_rw  = 0x41b60000;

        internal YacompStorage storage;
        internal struct_zip_end info_end;
        internal struct_zip_sign info_sign;
        internal api_compress_algo algo_defalte;
        internal api_compress_algo algo_none;
        internal Stream source_stream;
        internal bool disabled_ext = false;
        internal bool leave_open = false;
        public YacompZip(string zip_path) : this(File.Open(zip_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { }
        public YacompZip(Stream s, bool leaveOpen = false)
        {
            this.leave_open = leaveOpen;
            storage = new YacompStorage();
            algo_defalte = new deflate_algo();
            algo_none = new not_compress_algo();
            source_stream = s;
            read_central(source_stream);
            read_items(source_stream);
        }
        public YacompZip()
        {
            storage = new YacompStorage();
            algo_defalte = new deflate_algo();
            algo_none = new not_compress_algo();
        }
        public void Disabled_ExtInfo() {  disabled_ext = true; }
        public Stream ToStream()
        {
            var mem = new MemoryStream();
            ToStream(mem);
            return mem;
        }
        public void ToStream(Stream mem)
        {
            byte[] ext_buf = new byte[0];
            mem.Seek(0, SeekOrigin.Begin);
            mem.SetLength(0);
            var map = new Dictionary<string, uint>();

            storage.reset_algo(algo_defalte);
            foreach (var info in storage.LoopPaths())
            {
                map.Add(FastGuoMi.GetSM3(info.PathName), (uint)mem.Position);
                if (!disabled_ext) ext_buf = take_ntfs_utf8_ext(info.info.path_bytes);

                helper.Write(mem, item_tag);
                helper.Write(mem, ver_decompress_defalte);
                helper.Write(mem, flag_utf8);
                helper.Write(mem, compress_type_not_compress);
                helper.Write(mem, helper.read_msdos_time(info.EditTime));
                helper.Write(mem, helper.read_msdos_date(info.EditTime));
                helper.Write(mem, (uint)0);
                helper.Write(mem, (uint)0);
                helper.Write(mem, (uint)0);
                helper.Write(mem, info.info.path_bytes_size);
                helper.Write(mem, (ushort)ext_buf.Length);
                mem.Write(info.info.path_bytes, 0, info.info.path_bytes_size);
                mem.Write(ext_buf, 0, ext_buf.Length);
            }
            foreach (var info in storage.LoopFiles())
            {
                map.Add(FastGuoMi.GetSM3(info.FullPath), (uint)mem.Position);
                if (!disabled_ext) ext_buf = take_ntfs_utf8_ext(info.info.filepath_bytes);

                helper.Write(mem, item_tag);
                helper.Write(mem, ver_decompress_defalte);
                helper.Write(mem, flag_utf8);
                helper.Write(mem, compress_type_compress_deflate);
                helper.Write(mem, helper.read_msdos_time(info.EditTime));
                helper.Write(mem, helper.read_msdos_date(info.EditTime));
                helper.Write(mem, info.Crc32);
                helper.Write(mem, info.info.compress_size);
                helper.Write(mem, info.FileSize);
                helper.Write(mem, info.info.filepath_bytes_size);
                helper.Write(mem, (ushort)ext_buf.Length);
                mem.Write(info.info.filepath_bytes, 0, info.info.filepath_bytes_size);
                mem.Write(ext_buf, 0, ext_buf.Length);

                info.Compress(mem, true);
                helper.Write(mem, data_desc_tag);
                helper.Write(mem, info.Crc32);
                helper.Write(mem, info.info.compress_size);
                helper.Write(mem, info.FileSize);
            }

            var central_pos = (uint)mem.Position;

            foreach (var info in storage.LoopPaths())
            {
                if (!disabled_ext) ext_buf = take_ntfs_utf8_ext(info.info.path_bytes);

                helper.Write(mem, central_tag);
                helper.Write(mem, ver_compress_unix);
                helper.Write(mem, ver_decompress_defalte);
                helper.Write(mem, flag_utf8);
                helper.Write(mem, compress_type_not_compress);
                helper.Write(mem, helper.read_msdos_time(info.EditTime));
                helper.Write(mem, helper.read_msdos_date(info.EditTime));
                helper.Write(mem, (uint)0);
                helper.Write(mem, (uint)0);
                helper.Write(mem, (uint)0);
                helper.Write(mem, info.info.path_bytes_size);
                helper.Write(mem, (ushort)ext_buf.Length);
                helper.Write(mem, info.info.commit_size);
                helper.Write(mem, (ushort)0);
                helper.Write(mem, (ushort)0);
                helper.Write(mem, unix_attr_path_rwx);
                helper.Write(mem, map[FastGuoMi.GetSM3(info.PathName)]);
                mem.Write(info.info.path_bytes, 0, info.info.path_bytes_size);
                mem.Write(ext_buf, 0, ext_buf.Length);
                if (info.info.commit_size > 0) helper.write_utf8(mem, info.info.commit);
            }
            foreach (var info in storage.LoopFiles())
            {
                if (!disabled_ext) ext_buf = take_ntfs_utf8_ext(info.info.filepath_bytes);

                helper.Write(mem, central_tag);
                helper.Write(mem, ver_compress_unix);
                helper.Write(mem, ver_decompress_defalte);
                helper.Write(mem, flag_utf8);
                helper.Write(mem, compress_type_compress_deflate);
                helper.Write(mem, helper.read_msdos_time(info.EditTime));
                helper.Write(mem, helper.read_msdos_date(info.EditTime));
                helper.Write(mem, info.Crc32);
                helper.Write(mem, info.info.compress_size);
                helper.Write(mem, info.FileSize);
                helper.Write(mem, info.info.filepath_bytes_size);
                helper.Write(mem, (ushort)ext_buf.Length);
                helper.Write(mem, info.info.commit_size);
                helper.Write(mem, (ushort)0);
                helper.Write(mem, (ushort)0);
                helper.Write(mem, unix_attr_file_rwx);
                helper.Write(mem, map[FastGuoMi.GetSM3(info.FullPath)]);
                mem.Write(info.info.filepath_bytes, 0, info.info.filepath_bytes_size);
                mem.Write(ext_buf, 0, ext_buf.Length);
                if (info.info.commit_size > 0) helper.write_utf8(mem, info.info.commit);
            }
            var central_size = (uint)(mem.Position - central_pos);
            helper.Write(mem, end_tag);
            helper.Write(mem, (ushort)0);
            helper.Write(mem, (ushort)0);
            helper.Write(mem, (ushort)(storage.CalcFileCount() + storage.CalcPathCount()));
            helper.Write(mem, (ushort)(storage.CalcFileCount() + storage.CalcPathCount()));
            helper.Write(mem, central_size);
            helper.Write(mem, central_pos);
            helper.Write(mem, (ushort)0);
            mem.Flush();
        }

        internal byte[] take_unix_utf8_ext(byte[] filename_bytes) // utf8 兼容拓展  -未完成
        {
            var buf = new byte[24];
            var ix = 0;
            buf[ix++] = 0x75; buf[ix++] = 0x78; buf[ix++] = 0x0B; buf[ix++] = 0x0; buf[ix++] = 0x1; buf[ix++] = 0x4; buf[ix++] = 0x0; buf[ix++] = 0x0; buf[ix++] = 0x0; buf[ix++] = 0x0; buf[ix++] = 0x4; buf[ix++] = 0x0; buf[ix++] = 0x0; buf[ix++] = 0x0; buf[ix++] = 0x0; buf[ix++] = 0x55; buf[ix++] = 0x54; buf[ix++] = 0x5; buf[ix++] = 0x0; buf[ix++] = 0x1; buf[ix++] = 0xff; buf[ix++] = 0xff; buf[ix++] = 0xff; buf[ix++] = 0xff;
            return buf;
        }
        internal byte[] take_ntfs_utf8_ext(byte[] filename_bytes) // utf8 兼容拓展
        {
            byte[] buf;
            {
                var factory = new Queue<byte>();
                factory.Enqueue(0x0A);
                factory.Enqueue(0x00);
                factory.Enqueue(0x00);
                factory.Enqueue(0x00);
                factory.Enqueue(0x00);
                factory.Enqueue(0x00);
                for (var i = 0; i < filename_bytes.Length; i++)
                {
                    if ((filename_bytes[i] >> 7) == 0)
                    {
                        factory.Enqueue(filename_bytes[i]);
                        continue;
                    }
                    if ((filename_bytes[i] >> 5) == 0b110)
                    {
                        i += 1;
                        if (filename_bytes[i] == 0b11000000) factory.Enqueue(filename_bytes[i]);
                        else factory.Enqueue((byte)'?');
                        continue;
                    }
                    if ((filename_bytes[i] >> 4) == 0b1110)
                    {
                        factory.Enqueue((byte)'?');
                        i += 2;
                        continue;
                    }
                    if ((filename_bytes[i] >> 3) == 0b1110)
                    {
                        factory.Enqueue((byte)'?');
                        i += 3;
                        continue;
                    }
                }
                buf = factory.ToArray();
            }
            var size = buf.Length - 4;
            buf[2] = (byte)(size);
            buf[3] = (byte)(size >> 8);
            size -= 2;
            buf[4] = (byte)(size);
            buf[5] = (byte)(size >> 8);
            return buf;
        }
        public YacompStorage GetStorage() { return storage; }
        // 载入 中心目录区
        internal void read_central(Stream s)
        {
            s.Position = s.Length - 21;
            if (!helper.backto_flag_max65535(s, end_tag)) throw new ArgumentException("not normal zip file.");
            var endpos = s.Position - 4;

            info_end = read_end(s);
            s.Position = info_end.offset;
            while (s.Position < endpos)
            {
                var tag = helper.Readuint(s);
                switch (tag)
                {
                    case central_tag:
                        var p_info = read_central_item(s);
                        if (p_info.is_path())
                        {
                            var yacomp_p = storage.OperatePath(p_info.filename);
                            yacomp_p.info.time_edit = helper.read_datetime_by_msdos(p_info.edit_time, p_info.edit_date);
                        }
                        else
                        {
                            var yacomp_f = storage.OperateFile(p_info.filename, leave_open);
                            yacomp_f.info.time_edit = helper.read_datetime_by_msdos(p_info.edit_time, p_info.edit_date);
                            yacomp_f.info.compress_mem = source_stream;
                            switch (p_info.decompress_version)
                            {
                                case ver_decompress_none:
                                    yacomp_f.info.algo = algo_none; break;
                                default:
                                    yacomp_f.info.algo = algo_defalte; break;
                            }
                            yacomp_f.info.compress_tag = true;
                            yacomp_f.info.compress_size = p_info.compress_size;
                            yacomp_f.info.not_compress_size = p_info.not_compress_size;
                            yacomp_f.info.crc32 = p_info.crc32;
                            yacomp_f.info.offset = p_info.offset;
                            yacomp_f.info.data_offset = p_info.offset + 30 + p_info.filename_size + p_info.ext_size; // 推测
                        }
                        break;
                    case sign_tag:
                        info_sign = read_sign(s);
                        break;
                    default:
                        return;
                }
            }
        }

        internal void read_items(Stream s)
        {
            s.Position = 0;
            var len = s.Length;
            while (s.Position < info_end.offset)
            {
                var tag = helper.Readuint(s);
                switch (tag)
                {
                    case item_tag:
                        var file_info = read_item(s);
                        if (storage.Exist(file_info.filename))
                        {
                            if (storage.IsFile(file_info.filename))
                            {
                                var file = storage.OperateFile(file_info.filename, leave_open);
                                file.info.data_offset = file_info.offset;
                            }
                        }
                        break;
                    case data_desc_tag:
                        s.Position += 12;
                        break;
                    case 0:
                        continue;
                    default:
                        return;
                }
            }
        }

        internal struct_zip_item read_item(Stream s)
        {
            var file_info = new struct_zip_item { };
            file_info.version = helper.Readushort(s);
            file_info.flag = helper.Readushort(s);
            file_info.compress_type = helper.Readushort(s);
            file_info.edit_time = helper.Readushort(s);
            file_info.edit_date = helper.Readushort(s);
            file_info.crc32 = helper.Readuint(s);
            file_info.compress_size = helper.Readuint(s);
            file_info.not_compress_size = helper.Readuint(s);
            file_info.filename_size = helper.Readushort(s);
            file_info.ext_size = helper.Readushort(s);
            if (file_info.is_utf8()) file_info.filename = helper.Readutf8(s, file_info.filename_size);
            else file_info.filename = helper.Readchars(s, file_info.filename_size);
            file_info.ext = helper.Readbytes(s, file_info.ext_size);
            file_info.offset = s.Position;

            if (storage.Exist(file_info.filename))
            {
                if (storage.IsFile(file_info.filename))
                {
                    var file = storage.OperateFile(file_info.filename, leave_open);
                    if (file.info.compress_size == 0 || file.info.crc32 == 0) goto Read_File_01;

                    file_info.crc32 = file.info.crc32;
                    file_info.compress_size = file.info.compress_size;
                    file_info.not_compress_size = file.info.not_compress_size;
                }
                s.Position += file_info.compress_size;
                if (helper.Readuint(s) != data_desc_tag) s.Position -= 4;
                else s.Position += 12;

                return file_info;
            }


            Read_File_01:
            if (file_info.crc32 != 0 && file_info.compress_size != 0)
            {
                s.Position += file_info.compress_size;
                if (helper.Readuint(s) != data_desc_tag)
                {
                    s.Position -= 4;
                    return file_info;
                }
            }
            else if (storage.IsFile(file_info.filename) || !helper.goto_flag(s, data_desc_tag)) return file_info;

            file_info.crc32 = helper.Readuint(s);
            file_info.compress_size = helper.Readuint(s);
            file_info.not_compress_size = helper.Readuint(s);
            return file_info;
        }
        internal struct_zip_central read_central_item(Stream s)
        {
            var info = new struct_zip_central { };
            info.version = helper.Readushort(s);
            info.decompress_version = helper.Readushort(s);
            info.flag = helper.Readushort(s);
            info.compress_type = helper.Readushort(s);
            info.edit_time = helper.Readushort(s);
            info.edit_date = helper.Readushort(s);
            info.crc32 = helper.Readuint(s);
            info.compress_size = helper.Readuint(s);
            info.not_compress_size = helper.Readuint(s);
            info.filename_size = helper.Readushort(s);
            info.ext_size = helper.Readushort(s);
            info.commit_size = helper.Readushort(s);
            info.diskno = helper.Readushort(s);
            info.file_attr = helper.Readushort(s);
            info.out_file_attr = helper.Readuint(s);
            info.offset = helper.Readuint(s);
            if (info.is_utf8()) info.filename = helper.Readutf8(s, info.filename_size);
            else info.filename = helper.Readchars(s, info.filename_size);
            info.ext = helper.Readbytes(s, info.ext_size);
            info.commit = helper.Readutf8(s, info.commit_size);

            return info;
        }
        internal struct_zip_end read_end(Stream s)
        {
            var info = new struct_zip_end { };
            info.diskno = helper.Readushort(s);
            info.start_diskno = helper.Readushort(s);
            info.now_central_count = helper.Readushort(s);
            info.central_count = helper.Readushort(s);
            info.central_size = helper.Readuint(s);
            info.offset = helper.Readuint(s);
            info.commit_size = helper.Readushort(s);
            info.commit = helper.Readutf8(s, info.commit_size);

            return info;
        }
        internal struct_zip_sign read_sign(Stream s)
        {
            var info = new struct_zip_sign { };
            info.size = helper.Readushort(s);
            info.sign_data = helper.Readbytes(s, info.size);

            return info;
        }

        public void Dispose()
        {
            if (this.storage != null && !this.storage.is_disposed)
                this.storage.Dispose();
            if (!leave_open && source_stream != null)
                source_stream.Dispose();
            source_stream = null;
            this.storage = null;
        }
    }
}
